#include "TweakerTool.h"
#include "MoveCmd.h"
#include "ResizeCmd.h"
#include "MoveVertexCmd.h"
#include "../model/UMLBox.h"
#include "../model/UMLLine.h"

#include <QDebug>
#include <stdexcept>

using std::auto_ptr;

TweakerTool::TweakerTool(DiagramMan& dman)
:	Tool(dman),
	_blankState(this), _boxMoveState(this),
	_boxMoveBState(this),
	_boxMoveRState(this),
	_vertexMoveState(this),
	_state(&_blankState),
	_lastSel(NULL)
{
}

/*** Selection handlers ***/

bool TweakerTool::press(QMouseEvent& ev)
{
	SelectionMan& selection = dman().active().canvas().selection();

	/* If the user clicks in a new location, we want to select the item under
	 * the cursor immediately. This fulfills user expectations and makes
	 * drag-moving more sensible. */
	if (ev.button() == Qt::LeftButton && ev.pos() != _lastClick)
	{
		qDebug("Performing early select.");

		UMLDiagram& diagram = dman().active().diagram();

		QPoint logical = tological(ev.pos());

		/* Note: use rbegin() so that we select the most-recently-placed object
		 * first, a bit more user-friendly. */

		// Find the first object under cursor
		UMLDiagram::reverse_iterator i = find_if(
				diagram.rbegin(), diagram.rend(), object_contains(logical));

		// Don't do anything if we didn't find an object or if the object is
		// already selected (in which case we want click() to re-select).
		if (i != diagram.rend() && !selection.contains(*i))
		{
			// clear selection if CTRL is not held, but only if we're going to
			// actually select something else
			if (!ev.modifiers().testFlag(Qt::ControlModifier))
				selection.clear();

			_lastSel = *i;

			selection.toggle(*i);

			_lastClick = ev.pos();

			// if selection changed, we might want a new state
			updateState(ev.pos());

			return true;
		}
	}

	qDebug("Cancel early select.");

	/* This return lets click() handle the action if:
	 * 1) the user clicks again in the same place (for re-select)
	 * 2) there was no item under cursor to early-select
	 */
	return false;
}

void TweakerTool::click(QMouseEvent& ev)
{
	/* We only receive this notification if we didn't select on press(), so now
	 * re-select! */

	DiagramFam& active = dman().active();

	QPoint logical = tological(ev.pos());

	// clear selection if CTRL is not held
	if (!ev.modifiers().testFlag(Qt::ControlModifier))
		active.canvas().selection().clear();

	object_contains pred(logical);

	// Find the first object under cursor. Use rbegin() so that we select the
	// most-recently-placed object first to be a bit more user-friendly.
	UMLDiagram::reverse_iterator first = find_if(
			active.diagram().rbegin(), active.diagram().rend(), pred);

	// Find the iterator for _last, if it's under the cursor
	UMLDiagram::reverse_iterator i;
	for (i = first;
		 i != active.diagram().rend() && *i != _lastSel;
		 ++i)
		;

	// i now either points to _last or == rend()

	// Find one past _last
	if (i != active.diagram().rend())
	{
		qDebug("TweakerTool found last-selected item.");

		i = find_if(++i, active.diagram().rend(), pred);
	}

	// This conditional will first in two cases:
	// 1) If there is none past _last, loop: use first
	// 2) If _last was not under cursor: use whatever _is_ under cursor
	if (i == active.diagram().rend())
		i = first;

	if (i != active.diagram().rend())
	{
		_lastSel = *i;
		active.canvas().selection().toggle(*i);
	}

	_lastClick = ev.pos();
	updateState(ev.pos()); // if selection changed, we might want a new state
}

void TweakerTool::move(QMouseEvent& ev)
{
	updateState(ev.pos());
}

/*** State forwarders ***/

void TweakerTool::startDrag(QPoint& pt)
{
	_state->startDrag(pt);
}

void TweakerTool::drag(QMouseEvent& ev)
{
	_state->drag(ev);
}

void TweakerTool::endDrag(QMouseEvent& ev)
{
	_state->endDrag(ev);

	// TODO: switch states?
}

void TweakerTool::updateState(const QPoint& newpos)
{
	// Find the object under the cursor
	QPoint logical = tological(newpos);
	UMLObject* underCursor = dman().active().canvas().underPoint(logical);

	State* next = NULL;

	if (underCursor != NULL)
	{
		StateSwitchVisitor v(*this, logical);
		underCursor->accept(v);
		next = v.state();
	}

	if (next == NULL)
		next = &_blankState;

	/* Only switch states if it's different. Note that the Visitor will have
	 * updated state internals even if we don't actually switch states. */
	if (next != _state)
		next->takeControl();
}

/*** State ***/

TweakerTool::State::State(TweakerTool* context)
:	_context(context)
{
}

void TweakerTool::State::startDrag(QPoint& initial)
{
	_initialMouse = initial;

	// let subclass do whatever it needs to do
	init();
}

void TweakerTool::State::drag(QMouseEvent& ev)
{
	drag(ev.pos());
}

void TweakerTool::State::endDrag(QMouseEvent& ev)
{
	endDrag(ev.pos());
}

void TweakerTool::State::init()
{
	// default impl. does nothing
}

/*** LCState ***/

TweakerTool::LCState::LCState(TweakerTool* context)
:	State(context),
	_previous(-1, -1) // impossible previous position
{
}

void TweakerTool::LCState::drag(QMouseEvent& ev)
{
	QPoint logical = context().tological(ev.pos());

	if (logical != _previous)
	{
		drag(logical);
		_previous = logical;
	}
}

void TweakerTool::LCState::endDrag(QMouseEvent& ev)
{
	// Always call endDrag() regardless of whether logical pos changed.
	endDrag(context().tological(ev.pos()));

	_previous = QPoint(-1, -1);
}

/*** BlankState ***/

TweakerTool::BlankState::BlankState(TweakerTool* context)
:	State(context)
{
}

void TweakerTool::BlankState::takeControl()
{
	qDebug("BlankState taking over!");

	context().dman().active().canvas().setCursor(Qt::ArrowCursor);
	context()._state = this;
}

void TweakerTool::BlankState::drag(const QPoint&)
{
}

void TweakerTool::BlankState::endDrag(const QPoint&)
{
	// TODO
}

/*** BoxMoveState ***/

TweakerTool::BoxMoveState::BoxMoveState(TweakerTool* context)
:	LCState(context),
	// default for _previous
	_target(NULL)
	// default for _cmd
{
}

void TweakerTool::BoxMoveState::setTarget(UMLObject* obj)
{
	_target = obj;
}

void TweakerTool::BoxMoveState::takeControl()
{
	qDebug("BoxMoveState taking over!");

	context().dman().active().canvas().setCursor(Qt::OpenHandCursor);
	context()._state = this;
}

void TweakerTool::BoxMoveState::init()
{
	if (_target == NULL)
		throw new std::logic_error("Cannot move, no target!");

	// TODO when not sick: better way to do this?
	_initial= _target->position();
	_cmd.reset(new MoveCmd(_target, _initial));
}

void TweakerTool::BoxMoveState::drag(const QPoint& current)
{
	updateCmd(current);
	context().dman().active().acceptor().acceptTransitive(*_cmd);
}

void TweakerTool::BoxMoveState::endDrag(const QPoint& current)
{
	updateCmd(current);
	
	// Of course, send the command even if position didn't change.
	context().dman().active().acceptor().accept(auto_ptr<Command>(_cmd));
}

inline void TweakerTool::BoxMoveState::updateCmd(const QPoint& current)
{
	/* Use deltas from previous mouse position so that dragging from the
	 * bottom-right corner will move from that corner, not forcing the mouse to
	 * jump to top-left.
	 */
	
	QPoint delta = current - initialMouse();
	_cmd->setPos(_initial + delta);
}

/*** BoxMoveBState ***/

TweakerTool::BoxMoveBState::BoxMoveBState(TweakerTool* context)
:	LCState(context),
	_target(NULL)
	// default is fine for _cmd
{
}

void TweakerTool::BoxMoveBState::setTarget(UMLBox& box)
{
	_target = &box;
}

void TweakerTool::BoxMoveBState::takeControl()
{
	qDebug("BoxMoveBState taking over!");

	context().dman().active().canvas().setCursor(Qt::SizeVerCursor);
	context()._state = this;
}

void TweakerTool::BoxMoveBState::init()
{
	if (_target == NULL)
		throw std::runtime_error("Can't move bottom, no target box!");

	_original = _target->rect().size();
	_cmd.reset(new ResizeCmd(_target));
}

void TweakerTool::BoxMoveBState::drag(const QPoint& current)
{
	updateCmd(current);
	context().dman().active().acceptor().acceptTransitive(*_cmd);
}

void TweakerTool::BoxMoveBState::endDrag(const QPoint& current)
{
	updateCmd(current);
	context().dman().active().acceptor().accept(auto_ptr<Command>(_cmd));
}

inline void TweakerTool::BoxMoveBState::updateCmd(const QPoint& current)
{
	int delta = (current - initialMouse()).y();

	_cmd->setSize(_original + QSize(0, delta));
}

/*** BoxMoveRState ***/

TweakerTool::BoxMoveRState::BoxMoveRState(TweakerTool* context)
:	LCState(context),
	_target(NULL)
	// default is fine for _cmd
{
}

void TweakerTool::BoxMoveRState::setTarget(UMLBox& box)
{
	_target = &box;
}

void TweakerTool::BoxMoveRState::takeControl()
{
	qDebug("BoxMoveRState taking over!");

	context().dman().active().canvas().setCursor(Qt::SizeHorCursor);
	context()._state = this;
}

void TweakerTool::BoxMoveRState::init()
{
	if (_target == NULL)
		throw std::runtime_error("Can't move right, no target box!");

	_original = _target->rect().size();
	_cmd.reset(new ResizeCmd(_target));
}

void TweakerTool::BoxMoveRState::drag(const QPoint& current)
{
	updateCmd(current);
	context().dman().active().acceptor().acceptTransitive(*_cmd);
}

void TweakerTool::BoxMoveRState::endDrag(const QPoint& current)
{
	updateCmd(current);
	context().dman().active().acceptor().accept(auto_ptr<Command>(_cmd));
}

inline void TweakerTool::BoxMoveRState::updateCmd(const QPoint& current)
{
	int delta = (current - initialMouse()).x();

	_cmd->setSize(_original + QSize(delta, 0));
}

/*** VertexMoveState ***/

TweakerTool::VertexMoveState::VertexMoveState(TweakerTool* context)
:	LCState(context),
	_target(NULL),
	_vertex(0)
	// default good for _original
	// default good for _cmd
{
}

inline UMLLine* TweakerTool::VertexMoveState::target() const
{
	return _target;
}

inline void TweakerTool::VertexMoveState::setTarget(UMLLine* target)
{
	_target = target;
}

inline UMLLine::size_type TweakerTool::VertexMoveState::vertex() const
{
	return _vertex;
}

inline void TweakerTool::VertexMoveState::setVertex(UMLLine::size_type vertex)
{
	_vertex = vertex;
}

void TweakerTool::VertexMoveState::takeControl()
{
	qDebug("VertexMoveState taking over!");

	context().dman().active().canvas().setCursor(Qt::OpenHandCursor);
	context()._state = this;
}

void TweakerTool::VertexMoveState::init()
{
	_original = _target->vertex(_vertex);
	_cmd.reset(new MoveVertexCmd(_target, _vertex));
}

void TweakerTool::VertexMoveState::drag(const QPoint& current)
{
	updateCmd(current);
	context().dman().active().acceptor().acceptTransitive(command());
}

void TweakerTool::VertexMoveState::endDrag(const QPoint& current)
{
	updateCmd(current);
	context().dman().active().acceptor().accept(commandPtr());
}

Command& TweakerTool::VertexMoveState::command()
{
	return *_cmd;
}

std::auto_ptr<Command> TweakerTool::VertexMoveState::commandPtr()
{
	return std::auto_ptr<Command>(_cmd);
}

void TweakerTool::VertexMoveState::updateCmd(const QPoint& current)
{
	/* We could assume here that initialMouse() == _original, but I don't want
	 * to! */
	QPoint delta = current - initialMouse();
	_cmd->setPos(_original + delta);
}

/*** StateSwitchVisitor ***/

TweakerTool::StateSwitchVisitor::StateSwitchVisitor(
		TweakerTool& daddy, const QPoint& pt)
:	_daddy(daddy),
	_pt(pt),
	_state(NULL)
{
}

void TweakerTool::StateSwitchVisitor::visit(UMLBox& box)
{
	/* We know this will only be called if _pt is inside the box, so we just
	 * need to test if it's on the edge. */

	QRect rect = box.rect();

	bool bottom = _pt.y() == rect.bottom();
	bool right = _pt.x() == rect.right();

	if (bottom)
	{
		_daddy._boxMoveBState.setTarget(box);
		_state = &_daddy._boxMoveBState;
	}
	else if (right)
	{
		_daddy._boxMoveRState.setTarget(box);
		_state = &_daddy._boxMoveRState;
	}
	else
	{
		_daddy._boxMoveState.setTarget(&box);
		_state = &_daddy._boxMoveState;
	}
}

void TweakerTool::StateSwitchVisitor::visit(UMLLine& line)
{
	UMLLine::size_type vertex = line.vertex(_pt);

	if (vertex != UMLLine::NO_VERTEX)
	{
		VertexMoveState& s = _daddy._vertexMoveState;

		s.setTarget(&line);
		s.setVertex(vertex);
		_state = &s;
	}
	else
	{
		_daddy._boxMoveState.setTarget(&line);
		_state = &_daddy._boxMoveState;
	}
}

TweakerTool::State* TweakerTool::StateSwitchVisitor::state() const
{
	return _state;
}

